package service

import (
	"encoding/binary"
	"errors"
	"fmt"
	"github.com/goccy/go-json"
	"github.com/golang/protobuf/proto"
	"github.com/syndtr/goleveldb/leveldb"
	"github.com/syndtr/goleveldb/leveldb/opt"
	"go.uber.org/zap"
	"os"
	"sync"
	"time"
)

var db *leveldb.DB
var dbLock sync.Mutex
var ro *opt.ReadOptions
var wo *opt.WriteOptions

type ISet[T comparable] struct {
	m map[T]struct{}
}

func NewISet[T comparable]() *ISet[T] {
	return &ISet[T]{
		m: make(map[T]struct{}),
	}
}

func (s *ISet[T]) Add(value ...T) *ISet[T] {
	for _, val := range value {
		s.m[val] = struct{}{}
	}
	return s
}

func (s *ISet[T]) Remove(value ...T) *ISet[T] {
	for _, val := range value {
		delete(s.m, val)
	}
	return s
}

func (s *ISet[T]) Contains(value T) bool {
	_, ok := s.m[value]
	return ok
}

func Init(databaseFilePath string) (err error) {
	if err = os.MkdirAll(databaseFilePath, os.ModePerm); err != nil {
		err = fmt.Errorf("failed to create db dir: %w", err)
	}
	db, err = leveldb.OpenFile(databaseFilePath, nil)
	if err != nil {
		err = fmt.Errorf("failed to load db: %w", err)
		return
	}
	err = initTask()
	if err != nil {
		err = fmt.Errorf("failed to init task db: %w", err)
	}
	return
}

func UnixMilli() int64 {
	return time.Now().UnixMilli()
}

func PutInt64(batch *leveldb.Batch, key []byte, value int64) {
	bVal := binary.BigEndian.AppendUint64(nil, uint64(value))
	batch.Put(key, bVal)
}

func PutProto(batch *leveldb.Batch, key []byte, value proto.Message) error {
	bVal, err := proto.Marshal(value)
	if err != nil {
		return err
	}
	batch.Put(key, bVal)
	return nil
}

func GetInt64(key []byte) (int64, error) {
	bVal, err := Get(key)
	if err == nil {
		return int64(binary.BigEndian.Uint64(bVal)), nil
	} else {
		return 0, err
	}
}

func Put(key []byte, value []byte) error {
	return db.Put(key, value, wo)
}

func Get(key []byte) ([]byte, error) {
	return db.Get(key, ro)
}

func DbWithBatch(f func(*leveldb.Batch) error) error {
	dbLock.Lock()
	defer dbLock.Unlock()
	batch := new(leveldb.Batch)
	err := f(batch)
	if err == nil {
		return db.Write(batch, wo)
	}
	return err
}

func ExistKey(key []byte) bool {
	_, err := db.Get(key, ro)
	if err != nil {
		if !errors.Is(err, leveldb.ErrNotFound) {
			zap.L().Warn("failed to get obj", zap.Error(err), zap.String("key", string(key)))
		}
	}
	return err == nil
}

func SaveConfigData(key []byte, value interface{}) (err error) {
	var data []byte
	data, err = json.Marshal(value)
	if err != nil {
		return err
	}
	return Put(key, data)
}

func GetProto(key []byte, value proto.Message) (err error) {
	bValue, err := Get(key)
	if err == nil {
		err = proto.Unmarshal(bValue, value)
	}
	return
}

func GetJson(key []byte, value interface{}) (err error) {
	bValue, err := Get(key)
	if err == nil {
		err = json.Unmarshal(bValue, value)
	}
	return
}

func SaveProto(key []byte, value proto.Message) error {
	var data []byte
	data, err := proto.Marshal(value)
	if err != nil {
		return err
	}
	return Put(key, data)
}

func GetOrSaveConfig(key []byte, value interface{}) (err error) {
	if ExistKey(key) {
		return GetJson(key, value)
	} else {
		return SaveConfigData(key, value)
	}
}
